{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "331cKrixDlD_"
   },
   "source": [
    "# 循环神经网络的简洁实现\n",
    "\n",
    "本节将使用nn来更简洁地实现基于循环神经网络的语言模型。首先，我们读取周杰伦专辑歌词数据集。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "KjgM7FS7DlmV"
   },
   "outputs": [],
   "source": [
    "import d2ltorch as d2lt\n",
    "import math\n",
    "import torch\n",
    "from torch import nn, optim, utils\n",
    "import time\n",
    "\n",
    "(corpus_indices, char_to_idx, idx_to_char,\n",
    " vocab_size) = d2lt.load_data_jay_lyrics()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "PHZ2QxoGDnfp"
   },
   "source": [
    "## 定义模型\n",
    "\n",
    "nn的`RNN`模块提供了循环神经网络的实现。下面构造一个含单隐藏层、隐藏单元个数为256的循环神经网络层`rnn_layer`，并对权重做初始化。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "YwtZvHX0DpPv"
   },
   "outputs": [],
   "source": [
    "num_hiddens = 256\n",
    "rnn_layer = nn.RNN(vocab_size, num_hiddens)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "zCsUtoHzDsQD"
   },
   "source": [
    "接下来调用`rnn_layer`时如果没有传入隐藏状态`hx`，会使用`torch.zeros`函数初始化`hx`。它是一个形状为(隐藏层个数, 批量大小, 隐藏单元个数)的`tensor`。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "0YF_MM92Dt8s"
   },
   "source": [
    "与上一节中实现的循环神经网络不同，这里`rnn_layer`的输入形状为(时间步数, 批量大小, 输入个数)。其中输入个数即one-hot向量长度（词典大小）。此外，`rnn_layer`作为PyTorch的`nn.RNN`实例，在前向计算后会分别返回输出和隐藏状态，其中输出指的是隐藏层在各个时间步上计算并输出的隐藏状态，它们通常作为后续输出层的输入。需要强调的是，该“输出”本身并不涉及输出层计算，形状为(时间步数, 批量大小, 隐藏单元个数)。而`nn.RNN`实例在前向计算返回的隐藏状态指的是隐藏层在最后时间步的可用于初始化下一时间步的隐藏状态：当隐藏层有多层时，每一层的隐藏状态都会记录在该变量中；对于像长短期记忆这样的循环神经网络，该变量还会包含其他信息。我们会在本章的后面介绍长短期记忆和深度循环神经网络。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 35
    },
    "colab_type": "code",
    "id": "cuIE1wJqDx1m",
    "outputId": "25b5124e-e7e4-48af-eda2-9e4cb1322277"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(torch.Size([35, 2, 256]), 1, torch.Size([2, 256]))"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "batch_size = 2\n",
    "num_steps = 35\n",
    "X = torch.rand(num_steps, batch_size, vocab_size)\n",
    "Y, state_new = rnn_layer(X)\n",
    "Y.shape, len(state_new), state_new[0].shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "xfWgg90oD0TY"
   },
   "source": [
    "接下来我们继承`Module`类来定义一个完整的循环神经网络。它首先将输入数据使用one-hot向量表示后输入到`rnn_layer`中，然后使用全连接输出层得到输出。输出个数等于词典大小`vocab_size`。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "0t561BC6D2j8"
   },
   "outputs": [],
   "source": [
    "#本类已保存在d2ltorch包中方便以后使用\n",
    "class RNNModel(nn.Module):\n",
    "    def __init__(self, rnn_layer, num_hiddens, vocab_size, **kwargs):\n",
    "        super(RNNModel, self).__init__(**kwargs)\n",
    "        self.rnn = rnn_layer\n",
    "        self.vocab_size = vocab_size\n",
    "        self.linear = nn.Linear(num_hiddens, vocab_size)\n",
    "    \n",
    "    def forward(self, inputs, state=None):\n",
    "        # 将输入转置成(num_steps, batch_size)后获取one-hot向量表示\n",
    "        X = torch.stack(d2lt.to_onehot(inputs, self.vocab_size, inputs.device))\n",
    "        Y, state = self.rnn(X, state)\n",
    "        # 全连接层会首先将Y的形状变成(num_steps * batch_size, num_hiddens)，它的输出\n",
    "        # 形状为(num_steps * batch_size, vocab_size)\n",
    "        output = self.linear(Y.reshape(-1, Y.shape[-1]))\n",
    "        return output, state"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "_IoHibDAD4Od"
   },
   "source": [
    "## 训练模型\n",
    "\n",
    "同上一节一样，下面定义一个预测函数。这里的实现区别在于前向计算和初始化隐藏状态的函数接口。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "B_i-I9uND6m-"
   },
   "outputs": [],
   "source": [
    "# 本函数已保存在d2ltorch包中方便以后使用\n",
    "def predict_rnn_nn(prefix, num_chars, model, vocab_size, device, idx_to_char,\n",
    "                  char_to_idx):\n",
    "    # 使用model的成员函数来初始化隐藏状态\n",
    "    state = None\n",
    "    output = [char_to_idx[prefix[0]]]\n",
    "    with torch.no_grad():\n",
    "        for t in range(num_chars + len(prefix) - 1):\n",
    "            X = torch.Tensor([output[-1]]).to(device).reshape(1, 1)\n",
    "            (Y, state) = model(X, state) # 前向传播不需要传入模型参数\n",
    "            if t < len(prefix) - 1:\n",
    "                output.append(char_to_idx[prefix[t + 1]])\n",
    "            else:\n",
    "                output.append(int(Y.argmax(dim=1).item()))\n",
    "    return ''.join([idx_to_char[i] for i in output])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "ns79W2cIEAL3"
   },
   "source": [
    "让我们使用权重为随机值的模型来预测一次。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 35
    },
    "colab_type": "code",
    "id": "eWs89N77EBeF",
    "outputId": "37fe675e-569a-4a08-95ad-b78436de340f"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'分开每杰疗每杰每杰疗每杰'"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "device = d2lt.try_gpu()\n",
    "model = RNNModel(rnn_layer, num_hiddens, vocab_size)\n",
    "model.to(device)\n",
    "predict_rnn_nn('分开', 10, model, vocab_size, device, idx_to_char, char_to_idx)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "YBvAPyvyFcTn"
   },
   "source": [
    "接下来实现训练函数。算法同上一节的一样，但这里只使用了相邻采样来读取数据。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "TTa0jAasFdAh"
   },
   "outputs": [],
   "source": [
    "# 本函数已保存在d2ltorch包中方便以后使用\n",
    "def train_and_predict_rnn_nn(model, num_hiddens, vocab_size, device,\n",
    "                            corpus_indices, idx_to_char, char_to_idx,\n",
    "                            num_epochs, num_steps, lr, clipping_theta,\n",
    "                            batch_size, pred_period, pred_len, prefixes):\n",
    "    loss = nn.CrossEntropyLoss()\n",
    "    model.to(device)\n",
    "    d2lt.params_init(model, init=nn.init.normal_, mean=0.01)\n",
    "    optimizer = optim.SGD(model.parameters(), lr=lr, momentum=0, weight_decay=0)\n",
    "    \n",
    "    for epoch in range(num_epochs):\n",
    "        l_sum, n, start = 0.0, 0, time.time()\n",
    "        data_iter = d2lt.data_iter_consecutive(\n",
    "            corpus_indices, batch_size, num_steps, device)\n",
    "        state = None\n",
    "        for X, Y in data_iter:\n",
    "            optimizer.zero_grad()\n",
    "            if not state is None:\n",
    "                if isinstance(state, tuple):\n",
    "                    for s in state:\n",
    "                        s.detach_()\n",
    "                else:\n",
    "                    state.detach_()\n",
    "            (output, state) = model(X, state)\n",
    "            y = Y.long().t().flatten()\n",
    "            l = loss(output, y)\n",
    "            l.backward()\n",
    "            # 梯度裁剪\n",
    "            nn.utils.clip_grad_norm_(model.parameters(), clipping_theta)\n",
    "            optimizer.step()\n",
    "            l_sum += l.data.mean().item() * torch.numel(y.data)\n",
    "            n += torch.numel(y.data)\n",
    "        \n",
    "        if (epoch + 1) % pred_period == 0:\n",
    "            print('epoch %d, perplexity %f, time %.2f sec' % (\n",
    "                epoch + 1, math.exp(l_sum / n), time.time() - start))\n",
    "            for prefix in prefixes:\n",
    "                print(' -', predict_rnn_nn(\n",
    "                    prefix, pred_len, model, vocab_size, device, idx_to_char,\n",
    "                    char_to_idx))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "dLozqDDdFfUi"
   },
   "source": [
    "使用和上一节实验中一样的超参数来训练模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 277
    },
    "colab_type": "code",
    "id": "UlyADL2VFh--",
    "outputId": "c1b7531b-457b-4665-ecf7-397e649d9b9a"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch 50, perplexity 1.401943, time 0.09 sec\n",
      " - 分开 我有你寻身边 我池手 我有植身米 我想想想想你 我 我有多烦恼  没有你烦我有多烦恼多难熬  穿在\n",
      " - 不分开 我有样遇还我是你的想很久 黄想要有点寻天  你想这样没有你烦的我有多烦恼  没有你烦我有多烦恼多难\n",
      "epoch 100, perplexity 1.190715, time 0.08 sec\n",
      " - 分开 我不要再想你 爱情来的太快就像龙卷风 离不开暴风圈来不及逃 我不能再想 我不能再想 我不能够不远 \n",
      " - 不分开 我有多烦恼多难熬  穿过云层 我试著努力向你奔跑 爱才送到 你却已在别人怀抱 就是开不了口让她知道\n",
      "epoch 150, perplexity 1.149434, time 0.08 sec\n",
      " - 分开 我有你寄叹息 如果我遇见你是一场悲剧 我想我这辈子注定一个人演戏 最后再一个人慢慢的回忆 没有了过\n",
      " - 不分开 我有人有危险 一再重演 一根我不抽的菸 一放好多年 它一直在身边 干什么 干什么 我打开任督二脉 \n",
      "epoch 200, perplexity 1.162889, time 0.09 sec\n",
      " - 分开的开始 能 在隐着我 在 着  所 在的着我 肩膀 你 在我胸口睡著 像这样的生活 我爱你 你爱我 \n",
      " - 不分开的话像语言暴力 我已无能为力的提起 决定中断熟悉 然后在这里 不限日期 然后将过去 慢慢温习 让我爱\n",
      "epoch 250, perplexity 1.130298, time 0.09 sec\n",
      " - 分开 我不 我不要再想你 爱情来的太快就像龙卷风 离不开暴风圈来不及逃 我不能再想 我不 我不 我不 我\n",
      " - 不分开 我不 我不要再想 我不 我不 我不要再想你 爱情来的太快就像龙卷风 离不开暴风圈来不及逃 我不能再\n"
     ]
    }
   ],
   "source": [
    "num_epochs, batch_size, lr, clipping_theta = 250, 32, 1e2, 1e-2\n",
    "pred_period, pred_len, prefixes = 50, 50, ['分开', '不分开']\n",
    "train_and_predict_rnn_nn(model, num_hiddens, vocab_size, device,\n",
    "                            corpus_indices, idx_to_char, char_to_idx,\n",
    "                            num_epochs, num_steps, lr, clipping_theta,\n",
    "                            batch_size, pred_period, pred_len, prefixes)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "R4jrAxUUFkf6"
   },
   "source": [
    "## 小结\n",
    "\n",
    "* torch的`nn`模块提供了循环神经网络层的实现。\n",
    "* torch的`nn.RNN`实例在前向计算后会分别返回输出和隐藏状态。该前向计算并不涉及输出层计算。\n",
    "\n",
    "## 练习\n",
    "\n",
    "* 与上一节的实现进行比较。看看nn的实现是不是运行速度更快？如果你觉得差别明显，试着找找原因。\n",
    "\n",
    "\n",
    "## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/4089)\n",
    "\n",
    "![](../img/qr_rnn-gluon.svg)"
   ]
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "colab": {
   "collapsed_sections": [],
   "name": "rnn-nn.ipynb",
   "provenance": [],
   "version": "0.3.2"
  },
  "kernelspec": {
   "display_name": "Python [conda env:pytorch]",
   "language": "python",
   "name": "conda-env-pytorch-py"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.9"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {},
   "toc_section_display": true,
   "toc_window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
